import { JsMinifyOptions, minify as minifyTransform, transform, version } from '@swc/core'
import { CompluginInstance, CompluginOptions, createComplugin, SourceMap, utils } from 'complugin'
import { fromObject, fromSource, generateMapFileComment, removeComments } from 'convert-source-map'
import { Hash } from 'crypto'
import { isObject, isString } from 'lodash-es'
import { InternalModuleFormat } from 'rollup'
import { RawSource, Source, SourceMapSource } from 'webpack-sources'

type SwcOptions = Omit<import('@swc/core').Options, 'fileName' | 'include' | 'exclude'>

declare namespace compluginSwc {
  interface Options extends SwcOptions {
    /**
     * A minimatch pattern, or array of patterns,
     * which specifies the files in the build the plugin should ignore.
     *
     * When relying on Swc configuration files you can only exclude additional files with this option,
     * you cannot override what you have configured for Swc itself.
     *
     */
    include?: (string | RegExp)[]

    /**
     * A minimatch pattern, or array of patterns, which specifies the files in the build the plugin should operate on.
     * When relying on Swc configuration files you cannot include files already excluded there.
     */
    exclude?: (string | RegExp)[]

    /**
     * Use SWC to transform your code.
     * @default true
     */
    enableTransform?: boolean

    /**
     * Compress your code with SWC
     * @default false
     */
    minify?: boolean
  }
}

const _compluginSwc = createComplugin<compluginSwc.Options>({
  name: 'swc',
  enforce: undefined,
  factory(options, meta) {
    let CWD = process.cwd()
    if (meta.framework === 'esbuild') {
      CWD = meta.build.initialOptions.absWorkingDir ?? CWD
    }

    const {
      include: _include = [/\.(tsx?|jsx?|mjs)$/i],
      exclude: _exclude,
      minify,
      cwd,
      enableTransform = true,
      ..._swcOptions
    } = options ?? {}
    const swcOptions: SwcOptions = {
      ..._swcOptions,
      jsc: {
        target: 'es2020',
        loose: false,
        keepClassNames: true,
        parser: {
          syntax: 'typescript',
          tsx: true,
          decorators: true,
          dynamicImport: true
        },
        ..._swcOptions.jsc,
        minify: undefined,
        externalHelpers: true
      },
      module: {
        ..._swcOptions.module,
        type: 'es6',
        ignoreDynamic: true
      },
      sourceMaps: true,
      cwd: cwd ?? CWD
    }

    const minifyOptions: JsMinifyOptions | undefined = minify
      ? {
          ecma: 2020,
          ..._swcOptions.jsc?.minify
        }
      : undefined

    const pluginHooks: CompluginOptions = enableTransform
      ? {
          transformInclude: utils.createFilter(_include, _exclude),
          async transform(originalCode, id) {
            let { code, map } = await transform(originalCode, {
              ...swcOptions,
              sourceMaps: this.sourceMap,
              filename: id
            })
            if (isString(map)) {
              try {
                map = JSON.parse(map)
              } catch {
                map = undefined
              }
            }
            return { code, map }
          }
        }
      : {}

    if (minifyOptions) {
      switch (meta.framework) {
        case 'webpack':
          const { compiler } = meta
          const optimization = compiler.options.optimization
          if (optimization) {
            optimization.minimize = true
            optimization.minimizer = [
              {
                apply(compiler) {
                  const environment = compiler.options.output.environment
                  const devtool = compiler.options.devtool

                  if (environment) {
                    if (environment.bigIntLiteral || environment.dynamicImport) {
                      minifyOptions.ecma = 2020
                    } else if (
                      !environment.arrowFunction &&
                      !environment.const &&
                      !environment.destructuring &&
                      !environment.forOf &&
                      !environment.module
                    ) {
                      minifyOptions.ecma = 5
                    }
                  }

                  minifyOptions.sourceMap ??= !!devtool && devtool.includes('source-map')

                  compiler.hooks.compilation.tap('swc', compilation => {
                    const chunkHashHook =
                      compiler?.webpack?.javascript?.JavascriptModulesPlugin?.getCompilationHooks?.(
                        compilation
                      )?.chunkHash

                    const updateHash = (hash: Pick<Hash, 'update'>) => {
                      hash.update('swc-minify-plugin@' + version)
                    }

                    if (chunkHashHook) {
                      chunkHashHook?.tap('swc', (_, hash) => updateHash(hash as any))
                    } else {
                      const { mainTemplate, chunkTemplate } = compilation
                      mainTemplate.hooks.hashForChunk.tap('swc', updateHash)
                      chunkTemplate.hooks.hashForChunk.tap('swc', updateHash)
                    }

                    const processAssetsHook = compilation.hooks.processAssets

                    ;(processAssetsHook ?? compilation.hooks.optimizeChunkAssets).tapPromise(
                      processAssetsHook
                        ? {
                            name: 'swc',
                            stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE,
                            additionalAssets: true
                          }
                        : 'swc',
                      async assets => {
                        const promises: Promise<any>[] = []

                        const getInfo = compilation.getAsset
                          ? (fileName: string) => {
                              const info = compilation.getAsset(fileName)?.info
                              return Object.isFrozen(info) ? { ...info } : info
                            }
                          : () => ({} as never)

                        const updateAsset = compilation.updateAsset
                          ? (fileName: string, source: Source, info: any) => {
                              compilation.updateAsset(fileName, source as any, info)
                            }
                          : (fileName: string, source: Source, info: any) => {
                              compilation.assets[fileName] = source as any
                            }

                        for (const fileName in assets) {
                          if (!/\.(jsx?|mjs|cjs)$/.test(fileName)) return
                          const info = getInfo(fileName)
                          if (!info) continue
                          if (info.minimized || info.extractedComments) continue
                          const source = assets[fileName]
                          const inputCode = source.source().toString()
                          const inputMap = source.map({ columns: true })

                          promises.push(
                            minifyTransform(inputCode, minifyOptions).then(({ code, map }) => {
                              const newSource = map
                                ? new SourceMapSource(
                                    code,
                                    fileName,
                                    isString(map) ? JSON.parse(map) : map,
                                    inputCode,
                                    inputMap as any
                                  )
                                : new RawSource(code)

                              info.minimized = true

                              updateAsset(fileName, newSource, info)
                            })
                          )
                        }

                        await Promise.all(promises)
                      }
                    )

                    compilation.hooks.statsPrinter?.tap('swc', stats => {
                      stats.hooks.print
                        .for('asset.info.minimized')
                        .tap('swc-minify-plugin', (minimized, { green, formatFlag }) =>
                          minimized ? green?.(formatFlag ? formatFlag('minimized') : 'minimized') ?? '' : ''
                        )
                    })
                  })
                }
              }
            ]
          }
          break

        case 'esbuild':
          meta.build.initialOptions.minify = false

        default:
          pluginHooks.generateBundle = async (bundle, rollupBundle: import('rollup').OutputBundle) => {
            let format: InternalModuleFormat | 'esm' | undefined
            let isExternalMap = false
            let isRollupBundle = false

            switch (meta.framework) {
              case 'esbuild':
                format = meta.build.initialOptions.format
                minifyOptions.sourceMap = !!meta.build.initialOptions.sourcemap
                isExternalMap = meta.build.initialOptions.sourcemap === 'external'
                break
              case 'vite':
              case 'rollup':
                format = meta.outputOptions?.format
                minifyOptions.sourceMap = !!meta.outputOptions?.sourcemap
                isExternalMap = meta.outputOptions?.sourcemap === 'hidden'
                isRollupBundle = true
                break
            }

            if (format) {
              minifyOptions.module = format === 'es' || format === 'esm'
              if (format === 'cjs' || format === 'es' || format === 'esm') {
                minifyOptions.toplevel = true
              }
              const compressOptions = minifyOptions.compress
              if (isObject(compressOptions)) {
                compressOptions.module = minifyOptions.module
                compressOptions.toplevel ??= minifyOptions.toplevel
              }
            }

            const promises: Promise<any>[] = []

            if (isRollupBundle) {
              const bundle = rollupBundle
              for (const fileName in bundle) {
                const chunk = bundle[fileName]
                if (chunk?.type !== 'chunk') return
                if (!/\.(jsx?|mjs|cjs)$/.test(fileName)) return

                const { code: inputCode, map: inputMap } = chunk

                promises.push(
                  minifyTransform(inputCode, { ...minifyOptions, sourceMap: Boolean(inputMap) }).then(
                    ({ code, map }) => {
                      chunk.code = code
                      if (map) {
                        const newMap = new SourceMapSource(
                          code,
                          fileName,
                          isString(map) ? JSON.parse(map) : map,
                          inputCode,
                          inputMap
                        ).map({ columns: true })

                        if (newMap) {
                          Object.setPrototypeOf(newMap, Object.getPrototypeOf(inputMap))
                          chunk.map = (newMap as any) ?? undefined
                        }
                      }
                    }
                  )
                )
              }
            } else {
              for (const fileName in bundle) {
                if (!/\.(jsx?|mjs|cjs)$/.test(fileName)) return
                const chunk = bundle[fileName]
                if (!chunk || !chunk.meta) return

                const chunkSourceMap = bundle[fileName + '.map']
                let inputCode = chunk.text
                let inputMap: SourceMap | null | undefined

                if (chunkSourceMap) {
                  try {
                    inputMap = JSON.parse(chunkSourceMap.text)
                  } catch {}
                }

                inputMap = fromSource(inputCode)?.toObject() ?? inputMap
                inputCode = removeComments(inputCode)

                promises.push(
                  minifyTransform(inputCode, minifyOptions).then(({ code, map }) => {
                    if (map) {
                      const source = new SourceMapSource(
                        code,
                        fileName,
                        isString(map) ? JSON.parse(map) : map,
                        inputCode,
                        inputMap!
                      )
                      const generatedMap = source.map({ columns: true })
                      if (chunkSourceMap) {
                        chunk.text = isExternalMap ? code : code + '\n' + generateMapFileComment(fileName + '.map')
                        if (generatedMap) {
                          chunkSourceMap.text = JSON.stringify(generatedMap)
                        }
                      } else {
                        chunk.text = generatedMap ? code + '\n' + fromObject(generatedMap).toComment() : code
                      }
                    } else {
                      chunk.text = code
                    }
                  })
                )
              }
            }

            await Promise.all(promises)
          }
      }
    }
    return pluginHooks
  }
})

const generatorCache = new WeakMap<Function, Function>()
const instanceCache = new WeakMap<CompluginInstance, CompluginInstance>()
const isProxyKey = Symbol()

const VitePluginDisabledEsbuildAndMinify = (plugin: import('vite').Plugin, options?: compluginSwc.Options) => {
  const oldConfig = plugin.config
  let disabledEsbuild = ['typescript', undefined].includes(options?.jsc?.parser?.syntax)
  let disabledViteMinify = !options?.minify

  plugin.config = async function (this: any, config, env) {
    const newConfig: import('vite').UserConfig = { ...(await oldConfig?.call(this, config, env)) }
    if (disabledEsbuild) {
      newConfig.esbuild = false
    }
    if (disabledViteMinify) {
      ;(newConfig.build ?? (newConfig.build = {})).minify = false
    }

    return newConfig
  }

  return plugin
}

const compluginSwc = new Proxy(_compluginSwc, {
  get: (target, propertyKey, receiver) => {
    const result = Reflect.get(target, propertyKey, receiver)

    if (propertyKey === 'vite' && result) {
      let proxy = generatorCache.get(result)
      if (!proxy) {
        generatorCache.set(
          result,
          new Proxy(result, {
            apply: (target, thisArg, argArray) => {
              const plugin = Reflect.apply(target, thisArg, argArray)
              return VitePluginDisabledEsbuildAndMinify(plugin, argArray[0])
            }
          })
        )
      }

      return proxy
    }

    return result
  },
  apply: (target, thisArg, argArray) => {
    const instance = Reflect.apply(target, thisArg, argArray)
    if (isObject(instance) as any) {
      let proxy = instanceCache.get(instance)
      if (!proxy) {
        instanceCache.set(
          instance,
          (proxy = new Proxy(instance, {
            get: (target, propertyKey, receiver) => {
              const plugin = Reflect.get(target, propertyKey, receiver)

              if (propertyKey === 'vite' && !plugin[isProxyKey]) {
                Object.defineProperty(plugin, isProxyKey, { value: true })
                return VitePluginDisabledEsbuildAndMinify(plugin, argArray[0])
              }

              return plugin
            }
          }))
        )
      }

      return proxy
    }

    return instance
  }
})

// @ts-ignore
compluginSwc.default = compluginSwc

export = compluginSwc
